@import("js.annot");
@import("js.redis");
// @import("js.engine");

// @engine.ignoreStack();

const Types = require("./types.js");

const Connector = require("./connector.js");
const JSONRPCServer = require("./jsonrpc-server.js");
const RedisSession = require("./redis-session.js");
const CommonSession = require("./common-session.js");
const WSRPCServer = require("./wsrpc-server.js");

const reserved_names = ["sqlite", "connect", "id", "export", "snapshot", "find", "query", "create", "cache", "update", "services", "wrappers", "bindings", "id", "nextID", "models", "async", "options", "objects"];
const connection_pool = {};

const util = require("util");


var connect = function(context) {

    var documentPath = @kitty(context.file).mewchan.documentPath;

    if (!connection_pool[documentPath]) {
        throw new Error("未找到当前路径下的数据库链接 : " + documentPath);
    }

    return connection_pool[documentPath];
}


var dolabExportService = function(connection, domain, binding) {

    var namespaces = domain.split("@");

    if (namespaces.length > 2) {
        throw new Error("命名空间只能包含两层");
    }

    if (namespaces.reduce(function(empty, cvalue) {
            return empty || !cvalue;
        }, false)) {
        throw new Error("命名空间中存在空域");
    }

    if (!@.is(binding, Function)) {
        throw new Error("导出的服务不是一个函数 : " + domain);
    }

    if (reserved_names.indexOf(domain) >= 0) {
        throw new Error("导出服务名称不正确");
    } else {

        let exported = binding(connection);

        if (exported) {

            var id = @.uuid();

            connection.bindings[id] = binding;

            var last_scope = namespaces.reduce(function(scope, namespace, index) {

                if (scope.services[namespace]) {

                    if (index != namespaces.length - 1 && @.is(scope.services[namespace], String)) {
                        throw new Error("当前命名空间已经被定义为命令 : " + namespace);
                    }

                } else {
                    scope.root[namespace] = {};
                    scope.services[namespace] = {};
                }

                scope.path.push(namespace);

                return {
                    root: scope.root[namespace],
                    services: scope.services[namespace],
                    path: scope.path
                };

            }, {
                path: [],
                root: connection,
                services: connection.services,
            });

            Object.keys(exported).forEach(function(method) {

                if (last_scope.services[method] && @.is(last_scope.services[method], String)) {
                    throw new Error("当前访问路径已经被定义为其他命令 : " + last_scope.path.join(".") + "." + method);
                } else {
                    last_scope.services[method] = id;
                }

            });

            Object.assign(last_scope.root, exported);

            if (last_scope.path.length) {
                if (!connection.wrappers[last_scope.path.join("@")]){
                    @mew.emit("lab." + last_scope.path.join(".") + ".ready");
                }
                connection.wrappers[last_scope.path.join("@")] = last_scope.root;
            }

            return last_scope;

        } else {
            throw new Error("当前导出的服务返回为空 : " + domain);
        }


    }
}

var dolabExportTest = function(lab, binding, noRollback) {

    return @.async(function() {
        lab.snapshot(function(snapshot) {

            var testInstance = binding(snapshot);
            return @.async.all(@.keys(testInstance), function(name) {

                @info("lab test : [" + name + "] begin");

                try {
                    var result = testInstance[name]();

                    if (result && @.async.isAsync(result)) {

                        result.finished(function(error, result) {

                            if (error) {

                                @warn("lab test : [" + name + "] ends with error : ");
                                @error(error);

                            } else if (result) {

                                @celebr("lab test : [" + name + "] ends with result : ");
                                @dump(result);

                            } else {

                                @celebr("lab test : [" + name + "] ends successfully");

                            }

                            this.next();

                        }.bind(this));

                    } else if (result) {

                        @celebr("lab test : [" + name + "] ends with result : ");
                        @dump(result);

                    } else {
                        @celebr("lab test : [" + name + "] ends successfully");
                    }

                } catch (error) {

                    @warn("lab test : [" + name + "] ends with error : ");
                    @error(error);

                    this.next();
                }

            }).then(function() {

                @celebr("lab test finished");

                if (noRollback) {
                    this.next();
                } else {
                    this.reject("do rollback");
                }

            });
        }).finished(this.next);
    })


};


// TODO : fix folder scan file

@annot.func("mewchan.kitty", "lab.export.test", function(binding, noRollback) {

    if (@.process.debugging) {
        @celebr("start to run lab test :" + new Error().stack.split("\n").slice(3, 4));
    } else {
        @warn("please do not running lab test in non-debug mode :" + new Error().stack.split("\n").slice(3, 4));
        return @.async.resolve();
    }

    var connection = connect(this);
    var kitty = @kitty(this.file);
    var dirname = @.fs.dirname(this.file);

    if (@.is(binding, Function)) {

        return dolabExportTest(connection, binding, noRollback);

    } else if (@.is(binding, String)) {

        if (!@.fs.exists(binding) && @.fs.exists(@.fs.realOSPath(dirname, binding))) {
            binding = @.fs.realOSPath(dirname, binding);
        }

        if (@.fs.exists.file(binding) && @.fs.basename(binding)[0] != "." ) {

            if (@.fs.extname(binding) == ".js") {

                var filePath = @.fs.realOSPath(@.fs.dirname(binding), @.fs.basename(binding) + "." + kitty.mewchan.id + "-" + kitty.name.replace(".", "-") + ".mew-js");
                @annot.import(filePath, "mewchan");
                @annot.import(filePath, "mewchan.kitty");
                @annot.import(filePath, "mewchan.servlet");

                return dolabExportTest(connection, require(filePath), noRollback);

            } else {
                throw ("导出的服务文件后缀名错误 : " + @.fs.extname(binding));
            }

        } else if (@.fs.exists.dir(binding)) {

            return @.async.all(@.fs.scanFiles.sync(binding).filter(function(file) {

                return @.fs.extname(file.path) == ".js" && file.type == "file" && @.fs.basename(file.path)[0] != "." ;

            }).map(function(file) {

                return {
                    basename: @.fs.basename(file.path),
                    path: @.fs.realOSPath(binding, @.fs.basename(file.path) + "." + kitty.mewchan.id + "-" + kitty.name.replace(".", "-") + ".mew-js")
                };

            }), function(file) {

                @annot.import(file.path, "mewchan");
                @annot.import(file.path, "mewchan.kitty");
                @annot.import(file.path, "mewchan.servlet");
                dolabExportTest(connection, require(file.path), noRollback).pipe(this);

            });

        } else {
            throw new Error("导出的服务路径不存在");
        }

    } else {
        throw new Error("导出的测试定义错误");
    }
})

/**
 * @lab.user.connector.getUserInfo();
 * lab.hitomi.user.getUserInfo();
 */

@annot.func("mewchan.kitty", "lab.export.service", function(domain, binding) {

    try {
        if (domain && @.is(domain, String) && binding) {

            if (domain[0] == "@") {
                throw new Error("服务域名不能以@开头");
            }

            if (domain.indexOf(".") >= 0) {
                throw new Error("服务域名不能包含\".\"");
            }

            var connection = connect(this);
            var kitty = @kitty(this.file);
            var dirname = @.fs.dirname(this.file);

            if (util.types.isProxy(binding)) {
                
                if (domain[domain.length - 1] == "@") {
                    domain = domain.substr(0,domain.length - 1);
                }
                if (connection.services[domain]) {
                    throw new Error(domain + " 已经被定义成其他服务了");
                } else {
                    connection.services[domain] = @.uuid();
                    connection[domain] = binding;
                }
                
            } else if (@.is(binding, Function)) {

                dolabExportService(connection, domain, binding);

            } else if (@.is(binding, String)) {

                if (!@.fs.exists(binding) && @.fs.exists(@.fs.realOSPath(dirname, binding))) {
                    binding = @.fs.realOSPath(dirname, binding);
                }

                if (@.fs.exists.file(binding)) {

                    if (@.fs.extname(binding) == ".js") {

                        var filePath = @.fs.realOSPath(@.fs.dirname(binding), @.fs.basename(binding) + "." + kitty.mewchan.id + "-" + kitty.name + ".mew-js");
                        @annot.import(filePath, "mewchan.*");
                        dolabExportService(connection, domain, require(filePath));

                    } else {
                        throw ("导出的服务文件后缀名错误 : " + @.fs.extname(binding));
                    }

                } else if (@.fs.exists.dir(binding)) {

                    @.fs.scanFiles.sync(binding).reduce(function(cvalue, file) {

                        if (file.type == "file" && @.fs.extname(file.path) == ".js" && @.fs.basename(file.path)[0] != ".") {
                            let rootBasedPath = @.fs.rootBasedPath(binding, file.path);
                            rootBasedPath = rootBasedPath.split(/[\\\/]/);
                            if (rootBasedPath.length === 1) {
                                rootBasedPath = @.fs.basename(rootBasedPath[0]);
                            } else {
                                rootBasedPath = rootBasedPath[0];
                            }
                            cvalue.push({
                                basename: rootBasedPath,
                                path: @path(@.fs.dirname(file.path), @.fs.basename(file.path) + "." + kitty.mewchan.id + "-" + kitty.name.replace(".", "-") + ".mew-js")
                            });
                        }

                        return cvalue;

                    }, []).forEach(function(file) {

                        @annot.import(file.path, "mewchan");
                        @annot.import(file.path, "mewchan.kitty");
                        @annot.import(file.path, "mewchan.servlet");
                        @debug("import [" + file.path + "] as " + domain);
                        try {
                            if (domain[domain.length - 1] == "@") {
                                dolabExportService(connection, domain + file.basename, require(file.path));
                            } else {
                                dolabExportService(connection, domain, require(file.path));
                            }
                        } catch (ex) {
                            @error("error loading as service path : " + file.path);
                            @error(ex);
                        }
                    });

                    connection.last_update = Date.now();
                    connection.emit("change","service");

                } else {
                    throw new Error("导出的服务路径不存在");
                }

            } else {
                throw new Error("导出的服务定义错误");
            }

        } else {
            throw new Error("导出服务参数错误 ： 服务域名未找到");
        }
    } catch (ex) {
        @error(ex);
        throw ex;
    }
    
});


//return @lab.constant.coupon_status.received;

@annot.func("mewchan.kitty", "lab.export.constant", function(constants) {

    if (constants) {

        var connection = connect(this);

        var required = dolabRequireInput(connection, @.fs.dirname(this.file), constants);

        if (required && @.is(required, Object)) {

            @.keys(required).forEach(function(key) {

                if (connection.constants[key]) {
                    throw new Error("已存在同名常量 : @lab.constants." + key);
                } else {
                    connection.constants[key] = required[key];
                }

                @annot.marker("mewchan.kitty", "lab.constants." + key, function(fileLine, path, scope) {
                    return connect(this).constants[key];
                });

            });

            connection.last_update = Date.now();
            connection.emit("change","constant");

        } else {
            throw new Error("constants is not an object");
        }

    }
});


var dolabRequireInput = function(connector, dirname, input) {

    if (input && @.is(input, String)) {

        if (!@.fs.exists(input) && @.fs.exists(@.fs.realOSPath(dirname, input))) {
            input = @.fs.realOSPath(dirname, input);
        }

        if (@.fs.exists.file(input) && @.fs.basename(input)[0] != "." ) {

            if (@.fs.extname(input) == ".js" || @.fs.extname(input) == ".json") {

                if (connector.files[input]) {
                    return {};
                } else {
                    connector.files[input] = true;
                    return require(input) || {};
                }

            } else {
                throw new Error("输入路径错误 : " + @.fs.extname(input));
            }

        } else if (@.fs.exists.dir(input)) {

            return @.fs.scanFiles.sync(input).filter(function(file) {
                return file.type == "file" && (@.fs.extname(file.path) == ".js" || @.fs.extname(file.path) == ".json")  && @.fs.basename(file.path)[0] != ".";
            }).reduce(function(cvalue, file) {

                var filePath = @.fs.realOSPath(input, file.path);

                if (!connector.files[filePath]) {

                    connector.files[filePath] = true;
                    try {
                        var ivalue = require(filePath);

                        if (ivalue && @.is(ivalue, Object)) {
                            Object.assign(cvalue, ivalue);
                        }
                    } catch (ex) {
                        @warn(filePath);
                        @error(ex);
                    }
                    

                }

                return cvalue;
            }, {});

        } else {
            throw new Error("输入路径不存在 : " + input);
        }

    } else if (input && @.is(input, Object)) {
        return input;
    } else {
        throw new Error("输入参数错误 : " + input);
    }
}

@annot.func("mewchan.kitty", "lab.export.error", function(error) {


    var connection = connect(this);
    var errors = dolabRequireInput(connection, @.fs.dirname(this.file), error);

    if (errors && @.is(errors, Object)) {


        @.keys(errors).forEach(function(key) {

            if (connection.errors[key]) {
                throw new Error("已存在相同路径的错误 : @lab.errors." + key);
            } else {
                connection.errors[key] = new Error(errors[key].message || errors[key]);
            }

            if (errors[key].code) {

                if (connection.error_codes[errors[key].code]) {
                    throw new("已存在相同错误码的错误 : " + errors[key].code);
                }

                connection.errors[key].code = parseInt(errors[key].code);
                connection.error_codes[errors[key].code] = connection.errors[key];

            } else {
                connection.errors[key].code = 40000 + Object.keys(connection.error_codes).length;
                connection.error_codes[connection.errors[key].code] = connection.errors[key];
            }

            @annot.marker("mewchan.kitty", "lab.errors." + key, function(fileLine, path, scope) {

                var error_item = connect(scope).errors[key];
                var error = new Error(error_item.message);

                error.code = error_item.code;

                return error;
            });

            @annot.func("mewchan.kitty", "lab.error_codes." + connection.errors[key].code, function(message) {

                var error_item = connect(this).error_codes[connection.errors[key].code];

                var error = new Error(message || error_item.message);
                error.code = error_item.code;

                return error;
            });
        });

        connection.last_update = Date.now();
        connection.emit("change","error");

    } else {
        throw new Error("导出错误参数错误");
    }

});

@annot.marker("mewchan.kitty", "lab.sqlite", function(fileLine, path, scope) {
    return connect(scope).sqlite;
});

@annot.marker("js.common", "lab.types", function(fileLine, path, scope) {
    return Types;
});


@annot.func("mewchan.kitty", "lab", function(callback) {
    return @.async(function() {
        return @.async.ensure(callback(connect(this)));
    })

});

@annot.func("mewchan.kitty", "lab.create", function() {
    return connect(this).create.apply(connect(this), arguments);
});

@annot.func("mewchan.kitty", "lab.update", function() {
    return connect(this).update.apply(connect(this), arguments);
});

@annot.func("mewchan.kitty", "lab.save", function() {
    return connect(this).save.apply(connect(this), arguments);
});

@annot.func("mewchan.kitty", "lab.find", function() {
    return connect(this).find.apply(connect(this), arguments);
});

@annot.func("mewchan.kitty", "lab.snapshot", function(callback) {
    if (!callback || !@.is(callback, Function)) {
        throw new Error("callback format error");
    }
    return connect(this).snapshot(callback);
});

@annot.func("mewchan.kitty", "lab.init", function(options) {

    var kitty = @kitty(this.file);

    var documentPath = kitty.mewchan.documentPath;

    if (connection_pool[documentPath]) {
        return @.async.reject("document path : " + documentPath + " already exists");
    } else {

        // TODO: make the connection support more entrances
        connection_pool[documentPath] = new Connector(documentPath, options.database || {});

        return @.async(function() {
            connection_pool[documentPath].sqlite.open().pipe(this);
        }).then(function() {

            switch (connection_pool[documentPath].options.engine) {
                case "sqlite" : {
                    return connection_pool[documentPath].sqlite.execute("pragma journal_mode=WAL");
                }
                default : {
                    this.next();
                }
            }

        }).then(function() {

            if (options.redis){
                connection_pool[documentPath].redis.client = @redis.connect(options.redis.host, parseInt(options.redis.port || 6379));
                if (@.mem.redisCachePool) {
                    connection_pool[documentPath].cache_pool   = @.mem.redisCachePool({host : options.redis.host, port : parseInt(options.redis.port || 6379)});
                } else {
                    @warn("Redis cache pool not supported , upgrade mew_js to install the redis cache pool");
                    connection_pool[documentPath].cache_pool   = @.mem.cachePool();
                }
                
            } else {
                connection_pool[documentPath].cache_pool   = @.mem.cachePool();
            }
            
            if (options.jsonrpc) {

                @info("Binding JSONRPC [" + (options.jsonrpc.version || "2.0") + "] processor: " + (options.jsonrpc.path || "/gateway/api/jsonrpc.jsp"));
                
                let serverInfo = {
                    playground: options.jsonrpc.playground || "http.*",
                    path: options.jsonrpc.path || "/gateway/api/jsonrpc.jsp"
                }

                if (options.session && options.session.type) {

                    switch (options.session.type) {

                        case "redis": {

                            if (options.redis) {

                                if (!connection_pool[documentPath].redis.sessionManager){
                                    connection_pool[documentPath].redis.sessionManager = RedisSession(connection_pool[documentPath], options.session);
                                }
    
                                kitty.servlet(serverInfo.playground , "//*", {
                                    "type": "session",
                                    "options" : {
                                        "manager" : connection_pool[documentPath].redis.sessionManager
                                    }
                                },"");
    
                                break;
                            } else {
                                throw new Error("not support");
                            }
                        }
                        case "none":
                        default: {
                            
                        }
                    }

                } else {
                    kitty.servlet(serverInfo.playground, "//*", {
                        "type": "session",
                        "options" : {
                            "manager" : CommonSession(options.session || {})
                        }
                    }, "");
                }

                kitty.servlet(serverInfo.playground, serverInfo.path, {
                    "type": "cors",
                    "options": {
                        "justBrushIt": true
                    }
                }, "before");

                kitty.servlet(serverInfo.playground, serverInfo.path, JSONRPCServer(connection_pool[documentPath], options.jsonrpc), "after")
            
            }

            if (options.wsrpc) {

                @.async(function() {
                    
                    if (options.wsrpc.port) {
                        this.next(options.wsrpc.port);
                    } else {
                        @.net.availablePort(this.test);
                    }

                }).then(function(port) {

                    var wsPath = options.jsonrpc.path || "/gateway/api/wsrpc.jsp";
                    @info("Binding WSRPC [" + port + "] processor: " + wsPath);

                    @.net.wsServer({
                        "port": port,
                        "path": wsPath
                    }, WSRPCServer(connection_pool[documentPath], options.wsrpc));

                }).rejected(function(error) {
                    @error("wsrpc server init error");
                    @error(error);
                });

            }

            this.next();
        })
    }
});



@annot.func("mewchan.kitty", "lab.export.model", function(input) {

    var connection = connect(this);
    var dirname = @.fs.dirname(this.file);

    return @.async(function() {

        var superAsync = this;

        connection.async = connection.async.then(function() {
            var lockAsync = this;

            @.async(function() {

                process.nextTick(() => {
                    this.next(dolabRequireInput(connection, dirname, input));
                });

            }).then(function(models) {

                let indices = Object.create(null);
                connection.models = @.merge.simple(connection.models, models);
                connection.sqlite.orm.load(@.merge.simple(models, {
                    "!autogenerateTable": true
                })).then(function() {

                    var index_map = @.keys(models).reduce(function(cvalue, table) {

                        var indexed_field = @.keys(models[table]).filter(function(field) {
                            return models[table][field]["!indexed"];
                        });

                        if (indexed_field.length) {
                            cvalue[table] = indexed_field;
                        }
                        return cvalue;
                    }, {});

                    const escapeSQL = function(word) {

                        let delimiter = "\"";

                        if (connection.options.engine === "mysql") {
                            delimiter = "`";
                        }

                        return delimiter + word.split(delimiter).join(delimiter + delimiter) + delimiter;
                    };

                    @.async.all(@.keys(index_map), function(table) {
                        @.async.all(index_map[table], function(field) {
                            @.async(function() {

                                @.async(function() {
                                    @debug("Creating index: " + field + " on table : " + table);
                                    connection.sqlite.query(`create index ${escapeSQL("idx_" + field + "_" + table)} on ${escapeSQL(table)}(${escapeSQL(field)})`).pipe(this);
                                }).catch(this);

                            }).then(function(error) {

                                if (error) {

                                    if ((connection.options.engine === "mysql") && (error.code === 1061)) {

                                        this.next();
                                        return;

                                    } else if ((connection.options.engine === "sqlite") && (error.message.indexOf("exists") !== -1)) {

                                        this.next();
                                        return;

                                    }

                                    @warn(`Failed to create index[${"idx_" + field + "_" + table}]`);
                                    @error(error);

                                }
                                this.next();
                            }).pipe(this);
                        }).pipe(this);
                    }).pipe(this);

                }).pipe(this);

            }).finished(function(error) {

                if (error) {
                    superAsync.reject(error);
                } else {
                    connection.last_update = Date.now();
                    connection.emit("change","model");
                    superAsync.next();
                }

                lockAsync.next();
            })
        });

    });

});


module.exports = connection_pool;